using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;

namespace MyPhotoIndex.Utilities.ImageMetaData
{
    public class IPTC
    {
        private enum SegmentType { None = 0, keywords, Title, Description };
        private char[] Photoshop_8BIM = { '8', 'B', 'I', 'M' };
        private const int M_EOI = 0xD9;
        private const int M_APP13 = 0xed;
        private const int M_APP0 = 0xe0;
        private const int M_APP1 = 0xe1;

        private List<String> m_keywords  = new List<string>();
        private String m_title;
        private String m_description;

        public String Description
        {
            get { return m_description; }
            set { m_description = value; }
        }

        public String Title
        {
            get { return m_title; }
            set { m_title = value; }
        }

        public List<String> Keywords
        {
            get { return m_keywords; }
            set { m_keywords = value; }
        }

        public IPTC(String fileName)
        {
            try
            {
                LoadTags(fileName);
            }
            catch (Exception ex)
            {
                Logger.Log(ex);
            }
        }

        private void LoadTags(string fileName)
        {
            try
            {
                using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 200000))
                {
                    byte[] buffer = new byte[200000];
                    int bufferSize = fs.Read(buffer, 0, 200000);
                    using (MemoryStream ms = new MemoryStream(buffer))
                    {
                        int length = (int)ms.Length;

                        bool foundMarker = false;
                        while (length-- > 0)
                        {
                            int oneByte = GetNextMarker(ms);
                            if (oneByte == M_EOI)
                            {
                                foundMarker = true;
                                break;
                            }
                        }

                        if (foundMarker == false)
                        {
                            return;
                        }

                        while (length-- > 0)
                        {
                            int oneByte = -1;
                            //int oneByte = GetNextMarker(ms);

                            //if (oneByte == M_EOI)
                            { /* EOF */
                                while ((oneByte = ms.ReadByte()) == 0xff) ;
                                if (oneByte == -1)
                                {
                                    break;
                                }

                                switch (oneByte)
                                {
                                    case M_APP13:
                                        ExtractIPTC(ms);
                                        break;

                                    case M_APP0:
                                    /* APP0 is in each and every JPEG, so when we hit APP0 we insert our new APP13! */
                                    case M_APP1:
                                        ExtractIPTC(ms);
                                        break;
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Log(ex);
            }
        }

            private void ExtractIPTC(Stream fs)
            {
                byte[] dataLength = new byte[2];
                fs.Read(dataLength, 0, 2);

                //for these headers the length bytes are part of the length	
                int len = (int)((ulong)(dataLength[0] << 8) + ((ulong)(dataLength[1])) - 2);
                if (len <= 0)
                {
                    return;
                }

                byte[] IPTCData = new byte[len];
                fs.Read(IPTCData, 0, len);

                String xml = Encoding.ASCII.GetString(IPTCData, 0, len);
                string adobeNs = "http://ns.adobe.com/xap/1.0/\0";
                if (xml.StartsWith(adobeNs, StringComparison.CurrentCultureIgnoreCase) == true)
                {
                    xml = xml.Substring(adobeNs.Length).Trim();
                     XmlDocument xmlDoc = new XmlDocument();
                     try
                     {
                         XmlNamespaceManager NamespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
                         NamespaceManager.AddNamespace("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
                         NamespaceManager.AddNamespace("exif", "http://ns.adobe.com/exif/1.0/");
                         NamespaceManager.AddNamespace("x", "adobe:ns:meta/");
                         NamespaceManager.AddNamespace("xap", "http://ns.adobe.com/xap/1.0/");
                         NamespaceManager.AddNamespace("tiff", "http://ns.adobe.com/tiff/1.0/");
                         NamespaceManager.AddNamespace("dc", "http://purl.org/dc/elements/1.1/");
                         NamespaceManager.AddNamespace("xmp", "adobe:ns:meta/");

                         xmlDoc.LoadXml(xml);
                         XmlNode node = xmlDoc.SelectSingleNode("/xmp:xmpmeta/rdf:RDF/rdf:Description/dc:subject/rdf:Bag", NamespaceManager);
                         if (node != null)
                         {
                             foreach (XmlNode li in node)
                             {
                                 m_keywords.Add(li.InnerText);
                             }
                         }

                         node = xmlDoc.SelectSingleNode("/xmp:xmpmeta/rdf:RDF/rdf:Description/dc:title/rdf:Alt", NamespaceManager);
                         if (node != null)
                         {
                             m_title = node.ChildNodes[0].InnerText;
                         }

                         node = xmlDoc.SelectSingleNode("/xmp:xmpmeta/rdf:RDF/rdf:Description/dc:description/rdf:Alt", NamespaceManager);
                         if (node != null)
                         {
                             m_description = node.ChildNodes[0].InnerText;
                         }

                     }
                     catch (Exception ex)
                     {
                         Logger.Log(ex);
                     }
                }


                int pos = 0;
                while (pos < len - 12)
                {
                    if (IPTCData[pos] == Photoshop_8BIM[0] &&
                        memcmp(IPTCData, pos, Photoshop_8BIM, 4) == true)
                    {
                        pos += 4;
                        byte[] segmentType = new byte[2];
                        segmentType[0] = IPTCData[pos++];
                        segmentType[1] = IPTCData[pos++];

                        pos += 4;
                        int headerinfo = IPTCData[pos];

                        if (headerinfo != 0)
                        {
                            //PS6 style
                            // return buffer + pos + 4 + 2 + 1 + headerinfo + 2;//4 bytes 8BIM + 2 bytes type + 1 byte headerinfolen + headerinfo + 2 bytes 00 padding (?)
                        }
                        else
                        {
                            //old style
                            dataLength[0] = IPTCData[pos++];
                            dataLength[1] = IPTCData[pos++];
                            len = (int)((ulong)(dataLength[0] << 8) + ((ulong)(dataLength[1])));
                            byte[] tagData = new byte[len];
                            for(int i = 0; i < len; i++)
                            {
                                tagData[i] = IPTCData[pos+i];
                            }

                            int tagDataPosition = 0;
                            while (tagDataPosition < len)
                            {
                                if (tagData[tagDataPosition++] == 0x1c &&
                                    tagData[tagDataPosition++] == 0x02)
                                {
                                    segmentType[0] = tagData[tagDataPosition++];
                                    dataLength[0] = tagData[tagDataPosition++];
                                    dataLength[1] = tagData[tagDataPosition++];
                                    int segLen = (int)((ulong)(dataLength[0] << 8) + ((ulong)(dataLength[1])));

                                    String keyWord = Encoding.ASCII.GetString(tagData, tagDataPosition, segLen);
                                    
                                    SegmentType segment = SegmentType.None;
                                    if (segmentType[0] == 0x19)
                                    {
                                        segment = SegmentType.keywords;
                                    }
                                    else
                                    if (segmentType[0] == 0x5)
                                    {
                                        segment = SegmentType.Title;
                                    }
                                    if (segmentType[0] == 0x78)
                                    {
                                        segment = SegmentType.Description;
                                    }

                                    if (segment == SegmentType.keywords)
                                    {
                                        if (m_keywords.Count == 0)
                                        {
                                            m_keywords.Add(keyWord);
                                        }
                                    }
                                    else
                                    if (segment == SegmentType.Title)
                                    {
                                        if (String.IsNullOrEmpty(m_title))
                                        {
                                            m_title = keyWord;
                                        }
                                    }
                                    else
                                    if (segment == SegmentType.Description)
                                    {
                                        if (String.IsNullOrEmpty(m_description))
                                        {
                                            m_description = keyWord;
                                        }
                                    }
                                }
                            }
                        }
                    }
                    pos++;
                }
            }

            private bool memcmp(byte[] IPTCData, int pos, char[] data, int length)
            {
                int offset = 0;
                for (int i = pos; i < IPTCData.Length && offset < length; i++, offset++)
                {
                    if(IPTCData[i] !=  data[offset])
                    {
                        return false;
                    }
                }

                return true;
            }

            private int GetNextMarker(Stream fs)
            {
                /* skip unimportant stuff */
                int c = fs.ReadByte();

                if (c == -1)
                {
                    return M_EOI;
                }

                while (c != 0xff)
                {
                    if ((c = fs.ReadByte()) == -1)
                    {
                        return M_EOI; /* we hit EOF */
                    }
                }

                /* get marker byte, swallowing possible padding */
                do
                {
                    c = fs.ReadByte();
                    if (c == -1)
                    {
                        return M_EOI; /* we hit EOF */
                    }
                } while (c == 0xff);

                return c;
            }
    }
}
